package ripple.test.tools;

import ripple.client.RippleClient;
import ripple.common.DebugTool;
import ripple.common.DelayInjection;
import ripple.common.entity.Data;
import ripple.common.entity.NodeMetadata;
import ripple.server.RippleServer;
import ripple.server.core.overlay.FicusOverlay;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

public class Manager {

    private static String DATABASE_PATH = "data";


    public static String getDatabasePath() {
        return DATABASE_PATH;
    }
    public static void setDatabasePath(String databasePath) {
        DATABASE_PATH = databasePath;
    }

    private int cloudNodeNum;
    private int edgeNodeNum;
    private int clientPerServer;
    private String overlayType;

    int duration;
    int qps;
    int messageSize;


    public int getFaultNodeNumber() {
        return faultNodeNumber;
    }

    public List<NodeMetadata> getNodeList() {
        return nodeList;
    }

    private int faultNodeNumber;

//    private double failureProbability;

    private List<NodeMetadata> nodeList;
    private List<RippleServer> serverList;
    private List<RippleServer> cloudServerList;
    private List<RippleServer> edgeServerList;
    private List<RippleClient> clientList;


    public int getCloudNodeNum() {
        return cloudNodeNum;
    }
    public void setCloudNodeNum(int cloudNodeNum) {
        this.cloudNodeNum = cloudNodeNum;
    }
    public int getEdgeNodeNum() {
        return edgeNodeNum;
    }
    public void setEdgeNodeNum(int edgeNodeNum) {
        this.edgeNodeNum = edgeNodeNum;
    }
    public int getClientPerServer() {
        return clientPerServer;
    }
    public void setClientPerServer(int clientPerServer) {
        this.clientPerServer = clientPerServer;
    }

    public int getDuration() {
        return duration;
    }
    public void setDuration(int duration) {
        this.duration = duration;
    }
    public int getQps() {
        return qps;
    }
    public void setQps(int qps) {
        this.qps = qps;
    }
    public int getMessageSize() {
        return messageSize;
    }

    public void setMessageSize(int messageSize) {
        this.messageSize = messageSize;
    }



    public String getOverlayType() {
        return overlayType;
    }
    public void setOverlayType(String overlayType) {
        this.overlayType = overlayType;
    }

//    public double getFailureProbability() {
//        return failureProbability;
//    }


    public List<NodeMetadata> getNodelist() {
        return nodeList;
    }

    public void setNodeList(List<NodeMetadata> nodelist) {
        this.nodeList = nodelist;
    }


    public List<RippleServer> getServerList() {
        return serverList;
    }

    public void setServerList(List<RippleServer> serverlist) {
        this.serverList = serverlist;
    }


    public List<RippleServer> getCloudServerList() {
        return cloudServerList;
    }

    public void setCloudServerList(List<RippleServer> cloudServerList) {
        this.cloudServerList = cloudServerList;
    }

    public List<RippleServer> getEdgeServerList() {
        return edgeServerList;
    }

    public void setEdgeServerList(List<RippleServer> edgeSevrerList) {
        this.edgeServerList = edgeSevrerList;
    }


    public List<RippleClient> getClientList() {
        return clientList;
    }

    public void setClientList(List<RippleClient> clientList) {
        this.clientList = clientList;
    }


    public Manager () {
        this.setNodeList(new ArrayList<>());
        this.setServerList(new ArrayList<>());
        this.setEdgeServerList(new ArrayList<>());
        this.setCloudServerList(new ArrayList<>());
        this.setClientList(new ArrayList<>());
    }

    public void setCluster(int cloudNodeNum, int edgeNodeNum, int clientPerServer) {
        this.setCloudNodeNum(cloudNodeNum);
        this.setEdgeNodeNum(edgeNodeNum);
        this.setClientPerServer(clientPerServer);
    }

    public boolean setDelay(int E2EDelay, int E2CDelay, int C2CDelay) {
        DelayInjection.E2EDelay = E2EDelay;
        DelayInjection.E2CDelay = E2CDelay;
        DelayInjection.C2CDelay = C2CDelay;
        DelayInjection.BASE = FicusOverlay.MAX_SERVER_COUNT;
        DelayInjection.DIVIDER = FicusOverlay.MAX_CLOUD_GROUP;
        return true;
    }

    public void setLoad(int duration, int qps, int messageSize) {
        this.setDuration(duration);
        this.setQps(qps);
        this.setMessageSize(messageSize);
    }

//    public void setFailureProbability(double failureProbability) {
//        this.failureProbability = failureProbability;
//    }

    public void init () {
        this.initCluster();
    }

    public void init (int cloudNodeNum, int edgeNodeNum, int clientPerServer
            , String overlayType, int branch
            , int E2EDelay, int E2CDelay, int C2CDelay) {

        this.setCloudNodeNum(cloudNodeNum);
        this.setEdgeNodeNum(edgeNodeNum);
        this.setClientPerServer(clientPerServer);

        this.setOverlayType(overlayType);

        this.setDelay(E2EDelay, E2CDelay, C2CDelay);

        this.initCluster();
    }

    public boolean initCluster() {
        try {
            Files.createDirectories(Paths.get(DATABASE_PATH));

            for (int i = 0; i < cloudNodeNum; i++) {
                int cloudGroupId = FicusOverlay.MAX_SERVER_COUNT;
                int serverId = i + 1 + cloudGroupId;
                String storageLocation = DATABASE_PATH + "/server-" + serverId + ".db";
                String[] overlayInfo = this.getOverlayType().split(",");
                RippleServer rippleServer = switch (overlayInfo[0]) {
                    case "star" -> RippleServer.starProtocol(serverId, storageLocation);
                    case "tree" -> RippleServer.treeProtocol(serverId, storageLocation
                            , Integer.parseInt(overlayInfo[1]));
                    case "ficus" -> RippleServer.ficusProtocol(serverId, storageLocation
                            , FicusOverlay.CLOUD, cloudGroupId
                            , overlayInfo[1]);
                    default -> null;
                };

//                if (overlayInfo[0] == "star") {
//                    rippleServer = RippleServer.starProtocol(serverId, storageLocation);
//                } else if (overlayInfo[0] == "tree") {
//                    rippleServer = RippleServer.treeProtocol(serverId, storageLocation
//                            , Integer.parseInt(overlayInfo[1]));
//                } else if (overlayInfo[0] == "ficus") {
//                    if (overlayInfo[1] == "star") {
//                        RippleServer.ficusProtocol(serverId, storageLocation
//                                , FicusOverlay.CLOUD, cloudGroupId
//                                , overlayInfo[1]);
//                    } else if (overlayInfo[1] == "tree") {
//                        RippleServer.ficusProtocol(serverId, storageLocation
//                                , FicusOverlay.CLOUD, cloudGroupId
//                                , overlayInfo[1] + "," + overlayInfo[2]);
//                    }
//
//                }

                rippleServer.start();

                
                NodeMetadata nodeMetadata = new NodeMetadata(rippleServer.getId(), rippleServer.getAddress(), rippleServer.getApiPort());
                nodeMetadata.setType(FicusOverlay.CLOUD);
                nodeMetadata.setGroupId(cloudGroupId);

                this.getNodelist().add(nodeMetadata);
                this.getServerList().add(rippleServer);
                this.getCloudServerList().add(rippleServer);
            }

            for (int i = 0; i < edgeNodeNum; i++) {
                int edgeGroupId = 11 * FicusOverlay.MAX_SERVER_COUNT;
                int serverId = i + 1 + edgeGroupId;
                String storageLocation = DATABASE_PATH + "\\server-" + serverId + ".db";
                String[] overlayInfo = this.getOverlayType().split(",");
                RippleServer rippleServer = switch (overlayInfo[0]) {
                    case "star" -> RippleServer.starProtocol(serverId, storageLocation);
                    case "tree" -> RippleServer.treeProtocol(serverId, storageLocation
                            , Integer.parseInt(overlayInfo[1]));
                    case "ficus" ->
                            RippleServer.ficusProtocol(serverId, storageLocation
                                    , FicusOverlay.CLOUD, edgeGroupId
                                    , overlayInfo[1]);
                    default -> null;
                };
                rippleServer.start();

                NodeMetadata nodeMetadata = new NodeMetadata(rippleServer.getId(), rippleServer.getAddress(), rippleServer.getApiPort());
                nodeMetadata.setType(FicusOverlay.EDGE);
                nodeMetadata.setGroupId(edgeGroupId);

                this.getNodelist().add(nodeMetadata);
                this.getServerList().add(rippleServer);
                this.getEdgeServerList().add(rippleServer);
            }

            for (int i = 0; i < cloudNodeNum + edgeNodeNum; i++) {
                for (int j = 0; j < this.getClientPerServer(); j++) {
                    RippleServer rippleServer = serverList.get(i);
                    String storageLocation = DATABASE_PATH + "\\server-" + rippleServer.getId() + "-client-" + (j + 1) + ".db";
                    RippleClient rippleClient = new RippleClient(nodeList, storageLocation);
                    rippleClient.start();
                    this.getClientList().add(rippleClient);
                }
            }

            for (int i = 0; i < edgeNodeNum + cloudNodeNum; i++) {
                this.getServerList().get(i).initCluster(nodeList);
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }

        return true;
    }

    public void generateLoadOnEdge(int duration, int qps, int messageSize) {
        for (int i = 0; i < this.getEdgeNodeNum(); i++) {
            this.generateLoadOnEdgeNode(duration, qps, messageSize, i);
        }
//        int id = new Random().nextInt(this.getEdgeServerList().size());
//        this.generateLoadOnEdgeNode(duration, qps, messageSize, id);
    }

    public void generateLoadOnEdgeNode(int duration, int qps, int messageSize, int id){
        String topic = "topic";
        int sleepTime = 1000 / qps;

        for (int i = 0; i < duration * qps; i++) {
            this.getEdgeServerList().get(id).push(topic, new Data(PayloadGenerator.generateKey(messageSize)));
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

    public void stop() {
        for (RippleClient rippleClient : this.getClientList()) {
            rippleClient.stop();
        }
        for (RippleServer rippleServer : this.getServerList()) {
            rippleServer.stop();
        }
    }

    public void test(String testName){
        try {
            this.doTestOnce();

            Logger logger = new Logger(this);
            logger.recordResult(testName);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void doTestOnce() throws InterruptedException, IOException {
        Files.createDirectories(Paths.get(DATABASE_PATH));
        DebugTool.cleanDirectory(new File(DATABASE_PATH));

        this.init();
        Thread.sleep(1000);
        Scanner scanner = new Scanner(System.in);
        scanner.nextLine();

//        this.trackPush();

        this.generateLoadOnEdge(this.duration, this.qps, this.messageSize);
        Thread.sleep(1000);
        scanner.nextLine();

        this.stop();
    }

    private void trackPush() {
        for (RippleServer server : this.serverList) {
            server.push("Track", new Data("TrackData"));
        }
    }

    public void doTestOnce(String testName) throws InterruptedException, IOException {
        String dataBasePath = DATABASE_PATH;
        DATABASE_PATH = DATABASE_PATH + "/" + testName;
        this.doTestOnce();

        DATABASE_PATH = dataBasePath;

        Logger logger = new Logger(this);
        logger.recordResult(testName);
    }

    public void testMultiRound(String testName, int round) throws InterruptedException, IOException {
        Files.createDirectories(Paths.get(DATABASE_PATH));
        DebugTool.cleanDirectory(new File(DATABASE_PATH));



        for (int i = 0; i < round; i++) {
            this.doTestOnce("/round-" + round + "-db");
        }


    }

    public void testStarOverLay() {
        this.setOverlayType("star");
        this.test("testStarOverlay");
    }

    public void testTreeOverLay(int branch) throws InterruptedException {
        this.setOverlayType("tree," + branch);
        this.test("testTreeOverlay");
    }

    public void testFicusStarOverLay() throws InterruptedException {
        this.setOverlayType("ficus," + "star");
        this.test("testFicusStarOverlay");
    }


    public void testFicusTreeOverLay(int branch) throws InterruptedException {
        this.setOverlayType("ficus," + "tree," + branch);
        this.test("testFicusTreeOverlay");
    }


    public void testFault(int branch, int faultNodeNumber) {

        this.setOverlayType("tree," + branch);
        this.setFaultNodeNumber(faultNodeNumber);
        try {
            Files.createDirectories(Paths.get(DATABASE_PATH));
            DebugTool.cleanDirectory(new File(DATABASE_PATH));
    
            this.init();
            Thread.sleep(1000);


            Scanner scanner = new Scanner(System.in);
            scanner.nextLine();


            this.setFault();
            Thread.sleep(1000);
            scanner.nextLine();

    
    //        this.trackPush();
    
            for (int i = 0; i < this.getEdgeNodeNum(); i++) {
                if (this.getEdgeServerList().get(i).getNode().isRunning()) {
                    this.generateLoadOnEdgeNode(duration, qps, messageSize, i);
                }
                
                
            }
            Thread.sleep(1000);
            scanner.nextLine();
    
            this.stop();

            Logger logger = new Logger(this);
            logger.recordResult("testReform");
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }


    }

    public void testReform(int branch, int faultNodeNumber) {
        this.setEdgeNodeNum(this.getEdgeNodeNum() - faultNodeNumber);
        try {
            this.testTreeOverLay(branch);
        } catch (Exception e) {
            e.printStackTrace();
            
        }
        
    }

    // private void injectFault(int faultNodeNumber) {

    //    for(int i = 0; i < this.getEdgeNodeNum() * faultNodeNumber; i++) {
    //        serverList.get(faultNodeNumber).stop();
    //    }
    // }

//     public void testReform(int branch) {
//         this.setOverlayType("tree," + branch);
//         this.init();

// //        this.test("testReform: Initial State");
//         this.generateLoadOnEdge(this.duration, this.qps, this.messageSize);

//         Logger logger = new Logger(this);
//         logger.recordResult("Before Fault Injection");

//         logger.clean();

//         this.setFault();

//         this.generateLoadOnEdge(this.duration, this.qps, this.messageSize);

//         logger.recordResult("After Fault Injection, Before Reform");

//         logger.clean();

//         this.reform();

//         this.generateLoadOnEdge(this.duration, this.qps, this.messageSize);

//         logger.recordResult("After Reform");


//     }

    private void reform() {
        for (RippleServer server : this.getServerList()) {
            if (server.isRunning()) {
                server.getNode().reform();
                break;
            }
        }
    }

    private void setFault() {
        List<Integer> faultNodes = UniqueRandomNumber.getRandomNumbers(faultNodeNumber, 0, this.getServerList().size());
        for (int number : faultNodes) {
            this.getServerList().get(number).stop();
        }
    }

    public void setFaultNodeNumber(int faultNodeNumber) {
        this.faultNodeNumber = faultNodeNumber;
    }
}
